\chapter{插曲：文件和目录}
\thispagestyle{empty}

到目前为止，我们已经知道了操作系统的两个关键概念的发展：进程，CPU的虚拟化；以及地址空间，内存的虚拟化。两者协同，这两个抽象概念让程序可以在一个看似私有的、独立的世界中运行；就像它有自己的处理器和自己的内存一样。这种假象使得对系统编程更简单了，不管是桌面和服务器，还是在日益增长的包括移动电话的可编程平台上都很普遍。

在这章节中，我们向虚拟化难题中加了一段更加重要的片段：持久化存储。一个持久化存储设备会永久（至少是很长时间）地存储信息，比如典型的硬盘或者更先进的固态存储设备。不像内存断电之后内容就丢失，持久化存储设备会保持数据不丢。因此，操作系统必须额外关心这样的设备：这是用户保存他们真正关心的数据的地方。

crux：如何管理持久化存储设备
操作系统该如何管理持久化存储设备呢？需要哪些API呢？它的实现中有哪些重要的方面呢？

因此，在下面几节中，我们会探索一下管理持久数据的关键技术，专注与提高性能和可靠性的方法。然而，我们首先预览一下API：也就是与UNIX文件系统交互时你会看到的接口。

\section{文件和目录}

存储虚拟化的两个关键的抽象概念随着时间的推移已经有所发展了。第一个概念是文件。文件就是简单的字节的线性序列，每个文件都可以读写。每个文件都有某种底层名字，通常会有很多种；用户一般是感知不到这些名字的。由于历史原因，文件的底层名字经常被称作inode number。在未来的章节中我们会学习更多关于inode的内容；现在就假设每个文件都有一个与之相关联的inode号。

在大多数系统中，操作系统并不知道文件的结构（比如它是否是一个图片或者文本文件或者C代码）；相反，文件系统的责任只是简单地将这些数据持久地存储在磁盘上，并且确保当你再次请求这些数据时，你可以得到你第一次存在那儿的数据。要做到这些并不是看起来这么简单的！

第二个概念是目录。目录，像文件一样，也有底层名字（比如inode号），但是它的内容相当特殊：它包含一个<用户可读名字，底层名字>对的列表。比如，假设有一个文件的底层名字为『10』，给他一个用户可读的名字『foo』。foo文件所在的目录就会有一个目录项<“10”,”foo”>，将用户可读名字映射到底层的名字。目录中的每个目录项都指向一个文件或者其他目录。通过将多个目录存放在其他目录中，用户就可以勾走啊一个任意的目录树（或者层级目录），所有的文件和目录都存储在此目录树中。

层级目录起始于根目录（在基于UNIX的系统中，根目录命名为/），并使用某种分隔符命名接下来的子目录，直到想要的文件或者目录已经命名了。比如，如果用户在根目录中创建了一个目录foo，然后在foo中创建了一个文件bar.txt，我们可以通过它的绝对路径来指向该文件，即/foo/bar.txt。一个更复杂的目录树，见图39.1；此例中，有效的目录有/,/foo,/bar,/bar/bar,/bar/foo，有效的文件有/foo/bar.txt,/bar/foo/bar.txt。在文件系统树中，目录和文件只要不在位置，是可以有同名的（比如，图中有两个bar.txt文件，/foo/bar.txt和/bar/foo/bar.txt）。

目录树的图\\


TIP：认真思考命名
在计算机系统中，命名是很重要的一个方面【SK09】。在UNIX系统中，本质上，你所能想到的所有东西都是通过文件系统命名的。不仅仅是文件、设备、管道，甚至是进程【K84】都可以在看似普通的老文件系统中找到。这种统一命名简化了系统的概念模型，是的系统更简单，也更模块化。因此，不论什么时候创建一个系统或者接口，都要仔细考虑你在使用什么名字。

你也许注意到示例中的文件名有两部分：bar和txt，由一个点号分割开。第一部分是一个很随意的名字，而文件名的第二部分通常用来表示文件的类型，比如C代码(.c)，图片(.jpg)或者音乐文件(.mp3)。然而，这通常仅仅只是一个惯例：命名为main.c的文件中的数据一定是要C语言代码，这并不是强制性的。

因此，我们可以看到文件系统提供的一个很强大的东西：我们感兴趣的非常方便的命名所有文件的方法。在系统中，名字是很重要的，因为访问任何资源的第一步是可以给它命名。在UNIX系统中，文件系统提供了一个统一的访问磁盘、U盘、CD-ROM等设备上的文件，实际上，很多其他东西都安置在单个目录树下。


\section{文件系统接口}

我们来详细看看文件系统接口。我们会从基本的文件创建、访问、删除开始。你们可能会觉着这很简单，但是接着你会发现一个用来删除文件的神奇调用，叫做unlink()。希望，在本章结束时，这个神秘对你而言不再神秘。

\section{创建文件}

我们从最基本的操作说起：创建文件。这个操作可以通过open系统调用来完成；通过调用open()并传递O\_CREAT标志，程序就可以创建一个新文件了。下面是在当前工作路径中创建一个名为foo的文件的示例代码：\\
int fd = open("foo", O\_CREAT | O\_WRONLY | O\_TRUNC);\\
open()函数带了许多不同的标志。在此例中，程序创建了一个文件(O\_CREAT)，以此方式打开则仅可以写文件（O\_WRONLY），并且，如果文件已经存在的话，首先将其大小截断为0字节，因此会删掉已经有的内容（O\_TRUNC）。

ASIDE: creat()系统调用
旧时候创建文件是调用creat()，如下：\\
int fd = creat("foo");\\
你可以把creat()看做是带O\_CREAT|OWRONLY|O\_TRUNC标志的open()函数。因为open()函数可以创建文件，creat()就不大用了（确实，它可以通过调用open()来实现）；然而，它在UNIX学说中还占有特殊的地位。特别地，当Ken Thompson被问道：如果他重新设计UNIX时会有什么不同时，他回答说：我会在拼creat时带个e。

open()一种重要的方面是它的返回值：文件描述符。文件描述符就是一个整数，每个处理器一个，用来在UNIX中访问文件；因此，一旦文件被打开了，如果你有权限的话，那么你就可以用文件描述符来读写该文件了。这样，文件描述符就是一个capability[L84]，比如，
an opaque handle that gives you the power to perform certain operations。另一种看待文件描述符的方式是，将其看做是某种类型文件对象的指针；一旦你拥有了该对象，你可以调用其他方法来访问该文件，比如read()和write()。下面我们会看到如何使用文件描述符。

\section{读写文件}

当有了一些文件时，我们当然会想去对他们进行读写。那我们从读取存在的文件开始。如果使用命令行的话，我们只需要使用cat程序将文件的内容转存至屏幕上。

prompt> echo hello > foo\\
prompt> cat foo\\
hello\\
prompt>\\

这段代码中，我们将echo的输出重定向到foo文件，这样它就会含有单词”hello”。我们接着使用cat来查看该文件的内容。但是，cat程序是如何访问foo文件的呢？

为了将其弄明白，我们使用非常有用的工具来跟踪程序产生的系统调用。在Linux中，这个工具是strace；其他系统中也有类似的工具（Mac OS X中是dtruss，其他UNIX变种是truss）。strace所做的是跟踪程序运行期间锁产生的系统调用，并将跟踪的结果成现在屏幕上。

TIP：使用strace
strace工具提供一种很好的方式来观察程序是如何工作的。通过运行strace，你可以追踪到程序产生了哪些系统调用，观察参数以及返回码，并能够大概的知道程序执行的状况。
这个工具还可以带一些非常有用的参数。例如，-f会追踪fork出来的子进程；-t reports the time of day at each call;-e trace=open,close,read,write仅仅追列举的系统调用而忽略其他所有的系统调用。还有很多其他非常有用的标志——详见手册页，并学会如何使用这么一个非常有用的工具。


这里是一个使用strace来查看cat的例子（为了可读性，省略了一些其他的调用）：

代码\\

cat做的第一件事儿就是打开要读的文件。这里我们需要注意几点；第一，这个文件仅仅是为读取而打开的（非写入），如O\_RDONLY标志所示；第二，使用了64bit的偏移量（O\_LARGEFILE）；第三，open()调用成功并返回了一个文件描述符，值为3。

为什么第一次调用open()的返回值是3而不是你所期望的0或者1呢？其实，每个进程开始时都会有三个已经打开的文件，标准输入（进程可读取来获取输入），标准输出（进程可写入以降信息输出到屏幕上），标准错误（进程可写入错误消息）。这些文件分别用文件描述符0、1和2来表示。因此，当你第一次打开其他文件（如上面的cat一般），可以几乎肯定的得到文件描述符3。【难道会有例外情况么？译者注】

在成功打开文件后，cat使用read()系统调用来重复的从文件中读取一些字节。read()的第一个参数是文件描述符，以此告诉文件系统要付去哪个文件；一个进程当然可以同时拥有多个文件，因此描述符使得操作系统知道读取哪个对应的文件。第二参数指向了一个缓冲区，该缓冲区是read()读取的结果将要放置的区域；在上面的系统调用记录中，strace将read的结果显示到了("hello")串中。第三个参数是缓冲区的大小，在这个例子中是4KB。read()调用返回成功，此处是返回它读取的字节数（6，包括“hello”的5个字母和一个换行符）。

此时，你会发现strace中另一个有趣的结果：一个单独的write()系统调用，写到文件描述符1。正如我们上面提到的，这个描述符是标准输出，因此适用于将单词"hello"写入到屏幕上，就像cat程序一样。但它是直接调用write()么？也许吧（如果它是高度优化的）。但如果不是，cat可能的操作是调用库函数printf()；在它内部，printf()处理完传给它的所有格式化的细节，最终在标准输出上调用write将结果打印到屏幕上。

cat程序接着尝试从文件中读取更多的内容，但用于文件中没有剩余的字节，read()返回0，然后程序就知道已经读取了整个文件了。因此，这个程序就对相应的文件描述符调用close()，表示已经处理完foo文件。这个文件就因此关闭了，那么对它的读取也就完成了。

写入文件也是通过类似的一系列步骤完成的。首先，有一个待写入的已打开的文件，然后调用write()，获取会重复调用以写入大文件，最后调用close()。使用strace来追踪写文件，比如你自己编写的程序，或者dd工具，例如，dd if=foo of=bar。



\section{非连续读写}

到目前为止，我们已经讨论了如何读写文件，但是所有访问都是顺序的；要么从头到尾读取一个文件，要么从头到尾写一个文件。

然而，能够在文件中某个特定的位置读取或写入是很有用的；比如，如果你对一个文本文件创建了一个索引，并用它来查找一个特定的单词，你也许最后从文件某个随机偏移处读取。要这么做，我们要使用lseek()系统调用。下面是函数原型：\\
off\_t lseek(int tildes, off\_t offset, int whence);

第一个参数是文件描述符。第二个参数是偏移量，表示文件中指定位置的文件偏移量。第三个参数，由于历史原因才命名为whence，决定使用哪种seek操作。从手册页中可知：
若whence为SEEK\_SET，偏移量设为offset字节。
若whence为SEEK\_CUR，偏移量设为当前位置加上offset字节。
若whence为SEEK\_END，偏移量设为文件大小加上offset字节。

正如你可以从这段描述看出的那样，进程打开的每个文件，操作系统都会记录一个"current"偏移量，决定了下次读取或写入文件要开始的位置。因此，part of the abstraction of an open if that it has a current offset, 通过二者其一的方法更新。第一种，当发生了读取或写入N个字节，N加到current偏移量上；因此每个read或write隐式地更新偏移量。第二种，lseek显式地更新上面指定的偏移量。

注意，lseek()调用与磁盘操作seek没有任何关系，seek是移动磁头臂。lseek()调用只是简单地改变内核中变量值；当执行IO时，取决于此头的位置，地盘可能也可能不执行seek操作来完成这个请求。

ASIDE：调用lseek()不执行磁盘seek


\section{fsync()实现及时写}

我们多数调用write()，只是告诉文件系统：在将来某个时间点，请将这些数据写入持久化存储。为了性能，文件系统会将写操作缓存在内存中一段时间（比如5秒或这30秒）；在稍后的时间点，该写操作会就会真的分发给存储设备。从调用程序的角度来看，写操作看似非常快速地完成了，仅有非常少的情况下会发生数据丢失（比如，）。

然而，某些应用有时要求不仅仅是这样的保障。比如，在数据库管理系统中，完整恢复协议的开发需要每次操作都有强制写入磁盘的能力。

为了支持这种类型的应用，大多数文件系统提供一些附加的控制API。在UNIX世界里，提供给应用的借口是fsync(int fd)。当进程针对特定文件描述符调用fsync()时，文件系统会把该文件描述符对应的所有脏数据（未写入的）写入磁盘。fsync()函数会在所有写操作完成时返回。

下面有个简单的例子说明如何使用fsync()。这段代码打开foo文件，往文件里写入一块数据。然后调用fsync()来确保数据立即强制写入了磁盘。一旦fsync()返回，应用就可以安全地结束了，也就是数据已经被持久化了（假设fsync()被正确地实现了）。

代码\\

有趣的是，这段代码并不能确保你所期望的效果；在某些情况下，你还需要对foo文件所在的目录调用fsync()。加上这一步，确保不仅是文件本身（指文件内容）写入了磁盘，而且文件也写入了磁盘，如果文件是新创建的，文件也是路目录的一部分。不奇怪，这类细节经常会忽视掉，同时导致许多应用级的bug[P+13]。

【译者注：这段是说，因为新创建文件会在其所在目录中增加新的目录项，如果仅对文件做fsync，而不对该目录做fsync，可能会发生数据丢失。比如，文件所在的目录的新增目录项在文件系统写入磁盘之前，计算机崩溃了，那么该文件的目录项就不存在了，意味着该文件不存在了，那么即使文件的数据存在也是找不到的。】

\section{文件重命名}

一旦有了个文件，那么能赋予文件一个不一样的名字有时挺有用的。在命令行中，这功能由mv命令完成；此例中，foo文件重命名为bar：\\
prompt> mv foo bar\\

用strace，我们可以看到mv使用系统调用rename（char *old, char *new)，恰好传入两个参数：文件的原名（old）和新名（new）。

rename()调用提供了个有趣的保证，即它的实现是原子的，考虑到系统崩溃可能崩溃；如果系统在重命名期间崩溃的话，那么该名字就可能是旧名字也可能是新名字，不会发生中间态。因此，rename()对于需要原子性地更新文件状态的应用来说就非常重要。

这里我们再specific一点。假设你在使用文件编辑器（比如emacs），并且你在文件中间插入了一行。文件名比如就叫做foo.txt。编辑器为确保更新后的文件包含原内容和新插入的行的做法可能如下（简化处理略去了错误检测）：

代码\\

此例中，编辑器做的很简单：以一个临时的文件名写入新版本的文件（foo.txt.tmp），使用fsync()强制刷入磁盘，接着，当应用程序确定新文件的元数据和内容都写到磁盘上时，将临时文件重命名为原文件名。最后一步，原子地将新文件交换到位，同时删掉旧版本的文件，因此就实现了原子地更新文件。

\section{获取文件信息}

除了访问文件，我们还希望文件系统维护所存储文件的相当数量的信息。我们通常称这些数据为元数据（metadata）。要查看某一文件的元数据，我们可以使用stat()和fstat()系统调用。这些系统调用以文件路径（或者文件描述符）为输入，然后填充一个如下的stat数据结构：

stat结构体\\

你们可以看到每个文件都有许多信息，包括文件大小（单位字节），底层名字（比如inode号），一些属主信息，一些访问时间、修改时间，以及一些其他信息。要查看这些信息，你可以使用命令行工具stat：

stat命令执行结果\\

事实证明，每个文件系统通常使用称作inode的结构体来维护这类信息。我们将在讨论文件系统实现时学习更多关于inode的内容。现在，你只需要将inode看作是文件系统维护的一个永久数据结构，它包含上述我们看到的那些信息。

\section{删除文件}
到目前为止，我们知道了怎么创建文件以及如何访问他们，无论顺序与否。但是如何删除文件呢？如果你用过UNIX，你可能觉得你知道：只要运行rm程序。但是rm使用了什么系统调用来删除文件呢？

那我们就用老朋友stace来一探究竟吧。此处，我们删除这个烦人的文件foo：

prompt> strace rm foo\\
...\\
unlink("foo")			= 0\\
...\\

我们已经从更总到的结果中删掉了许多不相关的内容，仅留下了一个名词看起来很神秘的系统调用unlink()。正如你看到的，unlink()仅将要删除的文件名作为参数，若成功就返回0。但是这就是我们很迷惑了：为什么这个系统调用叫做『unlink』呢？为什么不是『remove』或者『delete』。要明白这个疑惑，我们就不仅仅要理解文件，还要理解目录。

\section{创建目录}

除了文件，还有一些列的目录相关的系统调用，你可以使用他们来创建、读取或删除目录。注意，你可以从不去直接写目录；因为目录的格式文件系统元数据要考虑的，你可以仅仅通过在目录中创建文件、目录或者其他对象类型来实现非直接的更新。通过这种方式，文件系统可以确保目录的内容是可预期的。

要想创建目录，只有一个系统调用——mkdir()可以使用。与之同名的程序，可以用来创建一个目录，我们通过使用mkdir创建一个名叫foo的简单目录来看看发生了什么：

prompt> strace mkdir foo\\
...\\
mkdir("foo",0777)		= 0\\
...\\
prompt>\\

TIP：要警惕强大的命令
rm命令为我们提供了一个强大命令很好的例子，
The program rm provides us with a great example of powerful com- mands, and how sometimes too much power can be a bad thing. For example, to remove a bunch of files at once, you can type something like:

prompt> rm *\\

where the * will match all files in the current directory. But sometimes you want to also delete the directories too, and in fact all of their contents. You can do this by telling rm to recursively descend into each directory, and remove its contents too:

prompt> rm -rf *\\

Where you get into trouble with this small string of characters is when you issue the command, accidentally, from the root directory of a file sys- tem, thus removing every file and directory from it. Oops!

Thus, remember the double-edged sword of powerful commands; while they give you the ability to do a lot of work with a small number of keystrokes, they also can quickly and readily do a great deal of harm.


当目录被创建好了，则会称之为『空』目录，尽管它确实有一点点内容。特别地，一个空目录有两个目录项：其中一个目录项指向当前目录，另一个则指向了父目录。前者显示为”.”目录，后者则显示为”..”目录。你可以在使用ls命令时加上-a选项来查看这两个目录项：

ls -a结果\\

\section{读取目录}
现在我们创建了一个目录，我们可能也希望读取一下。确实，这就是ls命令做的事情。那我们也写一个自己的类似于ls的工具，来看看它是怎么工作的。

并不能像一个文件一样去打开一个目录，我们要使用另一套新的系统调用。这个程序使用三个系统调用来完成任务：opendir(),readdir(),cloasedir()，你会看到接口是由多简单；我们每次只使用简单的循环来读一个目录项，并将目录中的每个文件的文件名和inode号打印出来。

main函数\\

下面的声明展示了struct dirent数据结构中每个目录项中的可以获得的信息：

dirent结构体\\

由于目录列出的信息比较少（基本地，映射了名字和inode号，以及少量的其他细节），程序也许还要对每个文件调用stat()来获得更多的信息，比如文件长度或者其他详细信息。实际上，这就是当你传递-l参数时ls做的事情；可以尝试用strace来观察一下带-l参数和不带-l参数的结果。

\section{删除目录}
最后，你可以删除使用rmdir()来删除一个目录（被同名命令rmdir使用）。然而，不像文件删除，删除目录会更危险，因为你可能一条命令就删掉大量的数据。因此，rmdir()要求删除目录前目录是空的（即仅包含”.”和”..”）。如果你试图删除非空的目录，rmdir()调用则返回失败。

\section{硬连接}
现在，我你们回到为什么是通过unlink()来删除文件这个谜题上来，先来了解在文件系统树上创建一个新目录项的方法，通过另一个系统调用link()。link()系统调用有两个参数，一个旧路径名和一个新的路径名；当你将一个新文件名“链接”到旧文件上时，实质上你是用了另一种方式来指代相同的文件。命令行工具ln就是这个功能，如我们在例子中看到的：

prompt> echo hello > file\\
prompt> cat file\\
hello\\
prompt> ln file file2\\
prompt> cat file2\\
hello\\

这里，我们创建了一个文件，包含了一个单词“hello”，将其命名为file。然后我们用ln命令创建一个指向这个文件的硬链接。接着，我们可以通过打开file或者file2来查看这个文件。

link工作的原理就是简单的在目录中创建另一个名字，并将其指向与原来文件相同的inode号（低层文件名）。文件并没有以某种方式复制；而仅仅是有了两个高层次的文件名（file和file2）同时指向相同的文件。我们可以自己到目录中查看，打印出每个文件名对应的inode号：

prompt> ls -i file file2\\
67158084 file\\
67158084 file2\\
prompt>\\

ls命令加上-i选项，就打印出每个文件的inode号（以及文件名）。因此，你可以看见link真实的原理：仅仅创建了一个新的引用到相同的inode号（例子中是67158084）。

那么现在你也许开始明白为什么unlink()要称作unlink()了。当你创建一个文件时，实际上你做了两件事儿。首先，你创建了一个结构体（inode），该结构体会记录所有跟文件相关的信息，包括文件大小、文件块在磁盘上的位置等等。接着，你会链接一个人类可理解的名字到该文件上，并将该连接放入目录中。

创建了一个硬连接到文件、到文件系统中后，那么原始文件名（file）和新创建的文件名（file2）就没有区别了；确实，他们都是一样地链接到该文件元数据，即例子中的inode号为67158084的文件。

因此，要从文件系统中删除文件，我们会调用unlink()。在上述的例子中，我可以删除名字为file的文件，然后还能访问该文件：

prompt> rm file\\
removed 'file'\\
prompt> cat file2\\
hello\\

这能够有效的原因是，当文件系统删除file文件时，它会检测该inode号对应的引用计数。这个引用计数（有时也称作连接计数）允许文件系统记录有多少文件名链接到了该inode上。当调用unlink()时，它删除了人可阅读的文件名与给定inode号之间的连接，并将引用计数减一；仅当引用计数为0时，文件系统才会释放inode和对应的数据块，那么就真的删除了该文件。

当然你可以通过stat()来查看文件的引用计数。我们来看看当我们创建、删除文件硬连接时引用计数的值。在这个例子中，我们会创建三个链接到相同文件的硬连接，然后将它们删除。注意看连接计数！

stat命令结果\\

\section{符号链接}

还有另一种类型的非常有用的连接，即符号连接，也称为软连接。实际上，硬连接有时候是有限制的：你不能创建目录的硬连接（是为了防止你在目录树上创建循环连接）；你也不能创建一个指向另一个磁盘分区的硬连接（因为inode号仅在同一个文件系统中是唯一的，跨文件系统则不一定）；等等。因此，出现了一种称之为符号链接的新类型。

要创建一个软连接，你可以使用同一个命令ln，但是需要加上-s选项。下面是个例子：

prompt> echo hello > file\\
prompt> ln -s file file2\\
prompt> cat file2\\
hello\\

如你所见，创建一个软连接看起来跟创建硬连接很像，而且，原始文件现在即可以通过文件名file也可以通过符号链接file2访问。

除了表面上的类似，符号链接实际上跟硬连接有很大的区别。首先，符号链接本身就是个文件，只是类型不同。我们已经讲过了普通文件和目录；符号链接是文件系统中的第三种文件类型。下面看看stat命令作用在符号链接上的结果：

stat符号链接结果\\

通过ls命令也可以看到区别。如果你仔细看ls以长格式输出的结果的第一个字符，你会发现普通文件的最左边第一个字符是-（短横），软连接是l。你也可以看到符号链接的大小（此例中是4B），以及其所指向的文件（file文件）。

ls -al命令结果\\

file2的大小的是因为符号链接的格式，其将所指向的文件的路径作为文件数据。因为file2所指向的文件名字是file，所以file2的文件大小是4B。如果我们连接到一个长一些的路径名，软连接文件的大小就会大一些：

更长文件名的连接大小\\

最后，由于创建符号链接的方式，他们可能会引发成为悬挂引用的问题：

悬挂引用\\

如你所见，在这个例子中，与硬连接不同的是，删除文件名为file原始文件导致连接到的路径不存在。

\section{创建并挂载文件系统}
上面我们已经了解过了访问文件、目录以、硬连接以及符号链接等基本接口。但是，还有一个我们还需要讨论一个话题：如何将众多的底层文件系统组合称一个完整的目录树。这个工作是通过下面两步实现的，首先创建多个文件系统，然后挂载他们以使其可被访问。

为了创建文件系统，大多数文件系统都提供了一个工具来完成这个工作，通常称为mkfs（读作make fs）。其思路如下：为该工具一个设备（比如磁盘分区，/dev/sda1）以及一个文件系统类型(比如ext3)作为输入，它会简单的写入一个起始于根目录的空文件系统到该磁盘分区上。然后mkfs说，让这儿有个文件系统吧！

然而，一旦创建了那样的文件系统，它需要使该目录树可访问。这个任务则通过mount命令来完成（用同名的mount()系统调用来实现）。挂载要做的，就是将一个已有的目录作为目标挂载点，然后粘贴一个新的文件系统到目录树的那个挂载点上。

这里来个例子也许会更有用。设想我么有一个还未挂载的ext3文件系统，存储在磁盘分区/dev/sda1，该分区包含如下内容：一个根目录，包含两个子目录a和b，每个目录中都有一个叫做foo的文件。那么我们希望将该文件系统挂载到/home/users。我们需要输入下面的命令：

prompt> mount -t ext3 /dev/sda1 /home/users\\

如果成功了，那么mount命令就可以是的这个新文件系统可访问了。然而，要注意是如何访问这个新文件系统的。要查看根目录的内容，我们要像下面那样使用ls：

prompt> ls /home/users\\
a b\\

如你所见，路径名/home/users/现在指向了这个新挂载的文件系统的根。类似的，我们可以通过路径/home/users/a和/home/users/b来访问文件a和b。最后，名为foo的文件可以通过/home/users/a/foo和/home/users/b/foo来访问。因此mount的优雅在于：不是创建许多独立的文件系统，而是将所有的文件系统统一到一个树中，使得可以统一的方便的命名。

要查看你的系统中挂载了那些文件系统以及其挂载点，可以简单的使用mount命令。你会看见如下的结果：

mount命令结果\\

上面混乱的内容说明了所有不同的文件系统，包括ext3（标准的基于磁盘的文件系统）、proc文件系统（访问所有进程相关信息的文件系统）、tmpfs（存放临时文件的文件系统）以及AFS（一个分布式文件系统），它们都挂载在这个及其的文件系统树上。


\section{小结}

UNIX系统的文件系统接口看似相当的原始，但是如果你想掌握的话，还有许多需要去理解。当然，没有什么比多多的使用它更有用了。去吧！当然，还需要多阅读；一般，Stevens[SR05]是比较好的开始。

我们已经了解了基本接口，还希望更深入的理解他们是如何工作的。更加有趣的是如何实现文件系统以达到API的要求，后面会详细的深入探索。

















